Skip to content

fix(dispatch-gates): mask # comments on shell-kind sources, so a shell target's inherited population is not prose - #16740

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-16132-shell-comment-mask-watch-hints
Sep 8, 2026
Merged

fix(dispatch-gates): mask # comments on shell-kind sources, so a shell target's inherited population is not prose#16740
baozhoutao merged 4 commits into
mainfrom
claude/issue-16132-shell-comment-mask-watch-hints

Conversation

@baozhoutao

@baozhoutao baozhoutao commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #16132

extractWatchHints masked its source with maskedModuleBody, which understands
line comments, block comments and a shebang — every comment syntax JavaScript
has. PROGRAM_TEXT_TARGET admits a third file kind, .sh, and a shell script's
prose is none of those three: on a shell-kind source every path a # comment
mentioned came back as a watch hint, because the JS-shaped literal regex reads a
backticked path in prose as a template literal and a double-quoted one as a
string.

Nothing widens. The instrument reports fewer hints on shell-kind sources and
the same hints everywhere else.

The card's fixture table, driven

All four against scripts/fixture.sh, on this branch:

hints  source
    0  # see `scripts/bump-objectui.selftest.sh` for the self-test   (comment, backtick prose)
    0  # see "scripts/bump-objectui.selftest.sh" for the self-test   (comment, quoted prose)
    1  bash 'scripts/bump-objectui.selftest.sh'                      (real invocation)
    0  // see `scripts/bump-objectui.selftest.sh`                    (JS comment — the control)

The card's own node -e criterion prints NOT CALIBRATED on be92d46 and
CALIBRATED here.

Why a per-line recogniser, and why the composition order is measured

A # opens a comment only at the start of a word and only outside quotes, so
$#, ${#a}, a#b and a # inside '…' or "…" are not comment starts — a
blank-from-#-to-end-of-line pass corrupts exactly the lines worth reading.
Quote tracking is therefore real, and it is where being wrong is unbounded: one
unbalanced quote silently disables the mask for the whole rest of the file, which
is the fabricating direction.

A cross-line scanner cannot keep shell's quoting straight. One was built and
measured on this tree before this one — with here-doc tracking and here-string
handling — and it still left 1,467 comment lines unmasked in
scripts/pm/os-verify-lock.sh and 8 in scripts/release-spec-changes.sh,
where the two surviving hints were the prose ones this card is about. The causes
were ordinary shell: command substitution restarts quoting, so "$(printf '%s' "$x")" reads as three spans to a scanner that does not model $( … ); and a
here-string (three less-than signs) looks like a here-doc introducer to anything
that matches the two-less-than form first.

So quote state is line-scoped: it opens and dies on its own line. Every
misreading is then bounded to the line it is on, and the residue lands in the
over-masking direction — a # line inside a here-doc body or a multi-line
quoted string is masked as if it were a comment. That is a missing lead on text
that is data rather than a path the script opens, which is the direction this
file errs in everywhere. It is pinned as a deliberate case so nobody "repairs" it
back into carried state.

The composition order is measured too, and it is the one that cannot widen.
Masking # first stops a # comment containing @objectstack/* from opening
a phantom JS block comment, which uncovers code below it and adds hints —
2 on this tree (scripts/downstream-smoke.sh, .claude/hooks/guard-tree-enum.sh).
Masking # last can only blank more of an already-masked body, so the shell
hint set is a subset of today's by construction.

The kind predicate is spelled as the difference of the two tests that already
exist — PROGRAM_TEXT_TARGET minus SCANNED_SOURCE_EXTENSIONS — rather than as
a third list, so a kind added to the follow arrives already masked: the widening
that admits it is the same edit that routes it here.

Blast radius, re-measured on be92d46

The card's numbers were taken on 1f2a02b. Re-measured on today's tree:

card (1f2a02b) this branch (be92d46)
tracked .sh files 25 27
spelling ≥ 1 hint 14 16
total hints as spelled 35 37
after the # mask 7 8
population 100 % prose 7/14 8/16
scripts/bump-objectui.sh 7 → 0 7 → 0

Tree-wide, over the 6,361 scanned program-text files: 11 files changed,
11 shrank, 0 grew; 6,885 → 6,856 hints. Every changed file is a .sh.

--json derivations for five shell-file surfaces (bump-objectui.sh,
vercel-ignore-docs.sh, pm/ensure-pm-labels.sh,
.claude/hooks/guard-governed-enqueue.sh, collect-release-notes.sh) are
byte-identical between be92d46 and this branch — today's output really is
unchanged, because 0 family-to-.sh population edges exist. This lands before
the card that would create the first one.

scripts/bump-objectui.sh reaching 0 is correct, and the reason is not the mask

The dispatch brief carried an assumption that 0 here is a red flag, because
.changeset/*.md has a genuine code site. The genuine site is real, and it was
already invisible to this scan before this change:

CS_FILE="${FRAMEWORK_ROOT}/.changeset/console-${SHORT}.md"

extractWatchHints admits a literal only if it matches ^[\w.@], and this one
begins with ${. Measured on be92d46, that line alone yields []; the same
line without the parameter expansion yields .changeset/console-x.md. So the
file's true population is non-empty and the scan correctly reports none of it —
a missing lead produced by a pre-existing admission rule, not by the mask. All
seven hints the file used to report were prose.

Tests

node scripts/pm/dispatch-gates.mjs --self-test1552 cases pass, 18 of
them new.

Ablation (one-time, run at 4bbf141; the only commit after it is four lines
of docblock prose): the wiring in extractWatchHints was mutated to
false && hashCommentProgram(scriptPath). The mutation was proved to reach disk
by blob hash (35bf7de859507bda) and by an anchor grep (removed 1 → 0,
injected 0 → 1) — not by an editor's exit code, which is 0 on zero matches. Under
it the card's criterion prints NOT CALIBRATED and the self-test exits 1 with
9 of the new cases red, including the live sweep, which then reads 37 spelled without it, 37 with. Restored with git checkout HEAD -- naming the
file — never the bare git checkout -- form, which takes the mutation straight
back out of the index — and verified by the blob hash returning to 35bf7de8
with git diff HEAD empty, not by the restore command's exit code.

Gates. Derived from the actual changed set with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands
(no staleness warning; --repo holds against this checkout's remote). The same
derivation run from a clean origin/main checkout as a control returns a
byte-identical 30-command list, so editing the tool did not move its own
answer. All 30 ran green — exit codes written to disk as each finished — and
--ran reconciles at b729acf: 30 derived, 30 run, 0 NOT-MEASURED, 0 UNRUN.

No changeset: scripts/pm/** publishes nothing, and this workflow's own comment
calls a PR that edits a CI-internal script the textbook skip-changeset case.

验收备注

Out of scope, filed as #16744 — a shell # comment containing @objectstack/*
opens a phantom JS block comment on a .sh source, and maskComments then
blanks every line down to the next terminator. Measured on this branch: 73,859
bytes of real shell code in scripts/pm/os-verify-lock.sh, 17,482 in
scripts/pm/os-regen-merge.sh, across 11 of the 27 tracked .sh files; two live
hints are lost to it, including the genuine
DEST="node_modules/@objectstack/spec/dist" in scripts/downstream-smoke.sh.

It is the cheap direction (a missing lead, which this tool errs in on purpose),
it is a defect in the JS masker applied to shell rather than in the hint scan,
and its remedy is how the composition order this PR rejected adds its 2 hints —
so fixing it here is exactly the widening ruling 3 forbids.
scripts/bump-objectui.sh is unaffected, checked because it is the file that
would carry the first family-to-.sh edge.


Generated by Claude Code

…ell target's inherited population is not prose

`extractWatchHints` masked its source with `maskedModuleBody`, which understands
every comment syntax JavaScript has. `PROGRAM_TEXT_TARGET` admits a third file
kind, `.sh`, whose prose is none of them: every path a `#` comment mentioned came
back as a watch hint, because the JS-shaped literal regex reads a backticked path
as a template literal and a double-quoted one as a string.

The mask is kind-scoped and composes ONTO the JS mask rather than replacing it,
so the shell hint set is a subset of today's by construction. Quote state is
line-scoped: a cross-line scanner desyncs on command substitution and on a `<<<`
here-string, and one unbalanced quote then disables the mask for the rest of the
file — the fabricating direction. The residue is a bounded over-mask.

Measured over the 27 tracked `.sh` files: 37 hints as spelled, 8 after the mask.
Tree-wide over 6,361 scanned files: 11 changed, 11 shrank, 0 grew. The live
derivation is byte-identical, because no family-to-`.sh` population edge exists
yet.

Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/m label Sep 8, 2026
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 8, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 8, 2026 03:18
@baozhoutao
baozhoutao enabled auto-merge September 8, 2026 03:18
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit 8ccf7a1 Sep 8, 2026
39 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-16132-shell-comment-mask-watch-hints branch September 8, 2026 03:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants